1
|
|
|
// @#ts-nocheck |
2
|
|
|
import {HttpFunction} from '@google-cloud/functions-framework/build/src/functions'; |
3
|
|
|
|
4
|
|
|
import { |
5
|
|
|
buildConfigurationForm, |
6
|
|
|
buildOptionsFromMessage, |
7
|
|
|
} from './config-form'; |
8
|
|
|
import {buildVoteCard} from './vote-card'; |
9
|
|
|
import {saveVotes} from './helpers/vote'; |
10
|
|
|
import {buildAddOptionForm} from './add-option-form'; |
11
|
|
|
import {callMessageApi} from './helpers/api'; |
12
|
|
|
import {addOptionToState} from './helpers/option'; |
13
|
|
|
import {buildActionResponse} from './helpers/response'; |
14
|
|
|
import {MAX_NUM_OF_OPTIONS} from './config/default'; |
15
|
|
|
import {splitMessage} from './helpers/utils'; |
16
|
|
|
import {chat_v1 as chatV1} from 'googleapis/build/src/apis/chat/v1'; |
17
|
|
|
import {Voter, Votes} from './helpers/interfaces'; |
18
|
|
|
|
19
|
|
|
export const app: HttpFunction = async (req, res) => { |
20
|
|
|
if (!(req.method === 'POST' && req.body)) { |
21
|
|
|
res.status(400).send(''); |
22
|
|
|
} |
23
|
|
|
const buttonCard: chatV1.Schema$CardWithId = { |
24
|
|
|
'cardId': 'welcome-card', |
25
|
|
|
'card': { |
26
|
|
|
'sections': [ |
27
|
|
|
{ |
28
|
|
|
'widgets': [ |
29
|
|
|
{ |
30
|
|
|
'buttonList': { |
31
|
|
|
'buttons': [ |
32
|
|
|
{ |
33
|
|
|
'text': 'Create Poll', |
34
|
|
|
'onClick': { |
35
|
|
|
'action': { |
36
|
|
|
'function': 'show_form', |
37
|
|
|
'interaction': 'OPEN_DIALOG', |
38
|
|
|
'parameters': [], |
39
|
|
|
}, |
40
|
|
|
}, |
41
|
|
|
}, |
42
|
|
|
{ |
43
|
|
|
'text': 'Terms and Conditions', |
44
|
|
|
'onClick': { |
45
|
|
|
'openLink': { |
46
|
|
|
'url': 'https://absolute-poll.yaskur.com/terms-and-condition', |
47
|
|
|
}, |
48
|
|
|
}, |
49
|
|
|
}, |
50
|
|
|
{ |
51
|
|
|
'text': 'Contact Us', |
52
|
|
|
'onClick': { |
53
|
|
|
'openLink': { |
54
|
|
|
'url': 'https://absolute-poll.yaskur.com/contact-us', |
55
|
|
|
}, |
56
|
|
|
}, |
57
|
|
|
}, |
58
|
|
|
], |
59
|
|
|
}, |
60
|
|
|
}, |
61
|
|
|
], |
62
|
|
|
}, |
63
|
|
|
], |
64
|
|
|
}, |
65
|
|
|
}; |
66
|
|
|
const event = req.body; |
67
|
|
|
console.log(event.type, |
68
|
|
|
event.common?.invokedFunction || event.message?.slashCommand?.commandId || event.message?.argumentText, |
69
|
|
|
event.user.displayName, event.user.email, event.space.type, event.space.name); |
70
|
|
|
console.log(JSON.stringify(event.message.cardsV2)); |
71
|
|
|
console.log(JSON.stringify(event.user)); |
72
|
|
|
let reply: chatV1.Schema$Message = { |
73
|
|
|
thread: event.message.thread, |
74
|
|
|
actionResponse: { |
75
|
|
|
type: 'NEW_MESSAGE', |
76
|
|
|
}, |
77
|
|
|
text: 'Hi! To create a poll, you can use the */poll* command. \n \n' + |
78
|
|
|
'Alternatively, you can create poll by mentioning me with question and answers. ' + |
79
|
|
|
'e.g *@Absolute Poll "Your Question" "Answer 1" "Answer 2"*', |
80
|
|
|
}; |
81
|
|
|
// Dispatch slash and action events |
82
|
|
|
if (event.type === 'MESSAGE') { |
83
|
|
|
const message = event.message; |
84
|
|
|
if (message.slashCommand?.commandId === '1') { |
85
|
|
|
reply = showConfigurationForm(event); |
86
|
|
|
} else if (message.slashCommand?.commandId === '2') { |
87
|
|
|
reply = { |
88
|
|
|
thread: event.message.thread, |
89
|
|
|
actionResponse: { |
90
|
|
|
type: 'NEW_MESSAGE', |
91
|
|
|
}, |
92
|
|
|
text: 'Hi there! I can help you create polls to enhance collaboration and efficiency ' + |
93
|
|
|
'in decision-making using Google Chat™.\n' + |
94
|
|
|
'\n' + |
95
|
|
|
'Below is an example commands:\n' + |
96
|
|
|
'`/poll` - You will need to fill out the topic and answers in the form that will be displayed.\n' + |
97
|
|
|
'`/poll "Which is the best country to visit" "Indonesia"` - to create a poll with ' + |
98
|
|
|
'"Which is the best country to visit" as the topic and "Indonesia" as the answer\n' + |
99
|
|
|
'\n' + |
100
|
|
|
'We hope you find our service useful and please don\'t hesitate to contact us ' + |
101
|
|
|
'if you have any questions or concerns.', |
102
|
|
|
}; |
103
|
|
|
} else if (message.text) { |
104
|
|
|
const argument = event.message?.argumentText?.trim().toLowerCase(); |
105
|
|
|
|
106
|
|
|
reply = { |
107
|
|
|
thread: event.message.thread, |
108
|
|
|
actionResponse: { |
109
|
|
|
type: 'NEW_MESSAGE', |
110
|
|
|
}, |
111
|
|
|
text: 'Hi! To create a poll, you can use the */poll* command. \n \n' + |
112
|
|
|
'Alternatively, you can create poll by mentioning me with question and answers. ' + |
113
|
|
|
'e.g *@Absolute Poll "Your Question" "Answer 1" "Answer 2"*', |
114
|
|
|
}; |
115
|
|
|
const choices = splitMessage(argument); |
116
|
|
|
if (choices.length > 2) { |
117
|
|
|
const pollCard = buildVoteCard({ |
118
|
|
|
choiceCreator: undefined, |
119
|
|
|
topic: choices.shift() ?? '', |
120
|
|
|
author: event.user, |
121
|
|
|
choices: choices, |
122
|
|
|
votes: {}, |
123
|
|
|
anon: false, |
124
|
|
|
optionable: true, |
125
|
|
|
}); |
126
|
|
|
const message = { |
127
|
|
|
cardsV2: [pollCard], |
128
|
|
|
}; |
129
|
|
|
reply = { |
130
|
|
|
thread: event.message.thread, |
131
|
|
|
actionResponse: { |
132
|
|
|
type: 'NEW_MESSAGE', |
133
|
|
|
}, |
134
|
|
|
...message, |
135
|
|
|
}; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
if (argument === 'help') { |
139
|
|
|
reply.text = 'Hi there! I can help you create polls to enhance collaboration and efficiency ' + |
140
|
|
|
'in decision-making using Google Chat™.\n' + |
141
|
|
|
'\n' + |
142
|
|
|
'Below is an example commands:\n' + |
143
|
|
|
'`/poll` - You will need to fill out the topic and answers in the form that will be displayed.\n' + |
144
|
|
|
'`/poll "Which is the best country to visit" "Indonesia"` - to create a poll with ' + |
145
|
|
|
'"Which is the best country to visit" as the topic and "Indonesia" as the answer\n' + |
146
|
|
|
'\n' + |
147
|
|
|
'We hope you find our service useful and please don\'t hesitate to contact us ' + |
148
|
|
|
'if you have any questions or concerns.'; |
149
|
|
|
reply.cardsV2 = [buttonCard]; |
150
|
|
|
} else if (argument === 'test') { |
151
|
|
|
reply.text = 'test search on <a href=\'http://www.google.com\'>google</a> (https://google.com)[https://google.com]'; |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
} else if (event.type === 'CARD_CLICKED') { |
155
|
|
|
const action = event.common?.invokedFunction; |
156
|
|
|
if (action === 'start_poll') { |
157
|
|
|
reply = await startPoll(event); |
158
|
|
|
} else if (action === 'vote') { |
159
|
|
|
reply = recordVote(event); |
160
|
|
|
} else if (action === 'add_option_form') { |
161
|
|
|
reply = addOptionForm(event); |
162
|
|
|
} else if (action === 'add_option') { |
163
|
|
|
reply = await saveOption(event); |
164
|
|
|
} else if (action === 'show_form') { |
165
|
|
|
reply = showConfigurationForm(event, true); |
166
|
|
|
} |
167
|
|
|
} else if (event.type === 'ADDED_TO_SPACE') { |
168
|
|
|
const message: chatV1.Schema$Message = { |
169
|
|
|
text: undefined, |
170
|
|
|
cardsV2: undefined, |
171
|
|
|
}; |
172
|
|
|
const spaceType = event.space.type; |
173
|
|
|
if (spaceType === 'ROOM') { |
174
|
|
|
message.text = 'Hi there! I\'d be happy to assist you in creating polls to improve collaboration and ' + |
175
|
|
|
'decision-making efficiency on Google Chat™.\n' + |
176
|
|
|
'\n' + |
177
|
|
|
'To create a poll, simply use the */poll* command or click on the "Create Poll" button below. ' + |
178
|
|
|
'You can also test our app in a direct message if you prefer.\n' + |
179
|
|
|
'\n' + |
180
|
|
|
'Alternatively, you can ' + |
181
|
|
|
'You can also test our app in a direct message if you prefer.\n' + |
182
|
|
|
'\n' + |
183
|
|
|
'We hope you find our service useful and please don\'t hesitate to contact us ' + |
184
|
|
|
'if you have any questions or concerns.'; |
185
|
|
|
} else if (spaceType === 'DM') { |
186
|
|
|
message.text = 'Hey there! ' + |
187
|
|
|
'Before creating a poll in a group space, you can test it out here in a direct message.\n' + |
188
|
|
|
'\n' + |
189
|
|
|
'To create a poll, you can use the */poll* command or click on the "Create Poll" button below.\n' + |
190
|
|
|
'\n' + |
191
|
|
|
'Thank you for using our bot. We hope that it will prove to be a valuable tool for you and your team.\n' + |
192
|
|
|
'\n' + |
193
|
|
|
'Don\'t hesitate to reach out if you have any questions or concerns in the future.' + |
194
|
|
|
' We are always here to help you and your team'; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
message.cardsV2 = [buttonCard]; |
198
|
|
|
|
199
|
|
|
reply = { |
200
|
|
|
actionResponse: { |
201
|
|
|
type: 'NEW_MESSAGE', |
202
|
|
|
}, |
203
|
|
|
...message, |
204
|
|
|
}; |
205
|
|
|
} |
206
|
|
|
res.json(reply); |
207
|
|
|
}; |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Handles the slash command to display the config form. |
211
|
|
|
* |
212
|
|
|
* @param {object} event - chat event |
213
|
|
|
* @param {boolean} isBlank - fill with text from message or note |
214
|
|
|
* @returns {object} Response to send back to Chat |
215
|
|
|
*/ |
216
|
|
|
function showConfigurationForm(event: chatV1.Schema$DeprecatedEvent, isBlank = false) { |
217
|
|
|
// Seed the topic with any text after the slash command |
218
|
|
|
const message = isBlank ? '' : event.message?.argumentText?.trim() ?? ''; |
219
|
|
|
const options = buildOptionsFromMessage(message); |
220
|
|
|
const dialog = buildConfigurationForm(options); |
221
|
|
|
return { |
222
|
|
|
actionResponse: { |
223
|
|
|
type: 'DIALOG', |
224
|
|
|
dialogAction: { |
225
|
|
|
dialog: { |
226
|
|
|
body: dialog, |
227
|
|
|
}, |
228
|
|
|
}, |
229
|
|
|
}, |
230
|
|
|
}; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Handle the custom start_poll action. |
235
|
|
|
* |
236
|
|
|
* @param {object} event - chat event |
237
|
|
|
* @returns {object} Response to send back to Chat |
238
|
|
|
*/ |
239
|
|
|
async function startPoll(event: chatV1.Schema$DeprecatedEvent) { |
240
|
|
|
// Get the form values |
241
|
|
|
const formValues = event.common?.formInputs; |
242
|
|
|
const topic = formValues?.['topic']?.stringInputs?.value?.[0]?.trim() ?? ''; |
243
|
|
|
const isAnonymous = formValues?.['is_anonymous']?.stringInputs?.value?.[0] === '1'; |
244
|
|
|
const allowAddOption = formValues?.['allow_add_option']?.stringInputs?.value?.[0] === '1'; |
245
|
|
|
const choices = []; |
246
|
|
|
const votes: Votes = {}; |
247
|
|
|
|
248
|
|
|
for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) { |
249
|
|
|
const choice = formValues?.[`option${i}`]?.stringInputs?.value?.[0]?.trim(); |
250
|
|
|
if (choice) { |
251
|
|
|
choices.push(choice); |
252
|
|
|
votes[i] = []; |
253
|
|
|
} |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
if (!topic || choices.length === 0) { |
257
|
|
|
// Incomplete form submitted, rerender |
258
|
|
|
const dialog = buildConfigurationForm({ |
259
|
|
|
topic, |
260
|
|
|
choices, |
261
|
|
|
}); |
262
|
|
|
return { |
263
|
|
|
actionResponse: { |
264
|
|
|
type: 'DIALOG', |
265
|
|
|
dialogAction: { |
266
|
|
|
dialog: { |
267
|
|
|
body: dialog, |
268
|
|
|
}, |
269
|
|
|
}, |
270
|
|
|
}, |
271
|
|
|
}; |
272
|
|
|
} |
273
|
|
|
const pollCard = buildVoteCard({ |
274
|
|
|
topic: topic, choiceCreator: undefined, |
275
|
|
|
author: event.user, |
276
|
|
|
choices: choices, |
277
|
|
|
votes: votes, |
278
|
|
|
anon: isAnonymous, |
279
|
|
|
optionable: allowAddOption, |
280
|
|
|
}); |
281
|
|
|
// Valid configuration, build the voting card to display in the space |
282
|
|
|
const message = { |
283
|
|
|
cardsV2: [pollCard], |
284
|
|
|
}; |
285
|
|
|
const request = { |
286
|
|
|
parent: event.space?.name, |
287
|
|
|
requestBody: message, |
288
|
|
|
}; |
289
|
|
|
const apiResponse = await callMessageApi('create', request); |
290
|
|
|
if (apiResponse) { |
291
|
|
|
return buildActionResponse('Poll started.', 'OK'); |
292
|
|
|
} else { |
293
|
|
|
return buildActionResponse('Failed to start poll.', 'UNKNOWN'); |
294
|
|
|
} |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Handle the custom vote action. Updates the state to record |
299
|
|
|
* the user's vote then rerenders the card. |
300
|
|
|
* |
301
|
|
|
* @param {object} event - chat event |
302
|
|
|
* @returns {object} Response to send back to Chat |
303
|
|
|
*/ |
304
|
|
|
function recordVote(event: chatV1.Schema$DeprecatedEvent) { |
305
|
|
|
const parameters = event.common?.parameters; |
306
|
|
|
if (!(parameters?.['index'])) { |
307
|
|
|
throw new Error('Index Out of Bounds'); |
308
|
|
|
} |
309
|
|
|
const choice = parseInt(parameters['index']); |
310
|
|
|
const userId = event.user?.name ?? ''; |
311
|
|
|
const userName = event.user?.displayName ?? ''; |
312
|
|
|
const voter: Voter = {uid: userId, name: userName}; |
313
|
|
|
const state = JSON.parse(parameters['state']); |
314
|
|
|
|
315
|
|
|
// Add or update the user's selected option |
316
|
|
|
state.votes = saveVotes(choice, voter, state.votes, state.anon); |
317
|
|
|
|
318
|
|
|
const card = buildVoteCard(state); |
319
|
|
|
return { |
320
|
|
|
thread: event.message?.thread, |
321
|
|
|
actionResponse: { |
322
|
|
|
type: 'UPDATE_MESSAGE', |
323
|
|
|
}, |
324
|
|
|
cardsV2: [card], |
325
|
|
|
}; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Opens and starts a dialog that allows users to add details about a contact. |
330
|
|
|
* |
331
|
|
|
* @param {object} event the event object from Google Chat. |
332
|
|
|
* |
333
|
|
|
* @returns {object} open a dialog. |
334
|
|
|
*/ |
335
|
|
|
function addOptionForm(event: chatV1.Schema$DeprecatedEvent) { |
336
|
|
|
const card = event.message!.cardsV2?.[0]?.card; |
337
|
|
|
// @ts-ignore: because too long |
338
|
|
|
const stateJson = (card.sections[0].widgets[0].decoratedText?.button?.onClick?.action?.parameters[0].value || card.sections[1].widgets[0].decoratedText?.button?.onClick?.action?.parameters[0].value) ?? ''; |
339
|
|
|
const state = JSON.parse(stateJson); |
340
|
|
|
const dialog = buildAddOptionForm(state); |
341
|
|
|
return { |
342
|
|
|
actionResponse: { |
343
|
|
|
type: 'DIALOG', |
344
|
|
|
dialogAction: { |
345
|
|
|
dialog: { |
346
|
|
|
body: dialog, |
347
|
|
|
}, |
348
|
|
|
}, |
349
|
|
|
}, |
350
|
|
|
}; |
351
|
|
|
} |
352
|
|
|
; |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* Handle the custom vote action. Updates the state to record |
356
|
|
|
* the user's vote then rerenders the card. |
357
|
|
|
* |
358
|
|
|
* @param {chatV1.Schema$DeprecatedEvent} event - chat event |
359
|
|
|
* @returns {object} Response to send back to Chat |
360
|
|
|
*/ |
361
|
|
|
async function saveOption(event: chatV1.Schema$DeprecatedEvent) { |
362
|
|
|
const userName = event.user?.displayName ?? ''; |
363
|
|
|
const state = getEventPollState(event); |
364
|
|
|
const formValues = event.common?.formInputs; |
365
|
|
|
const optionValue = formValues?.['value']?.stringInputs?.value?.[0]?.trim() || ''; |
366
|
|
|
addOptionToState(optionValue, state, userName); |
367
|
|
|
|
368
|
|
|
const card = buildVoteCard(state); |
369
|
|
|
const message = { |
370
|
|
|
cardsV2: [card], |
371
|
|
|
}; |
372
|
|
|
const request = { |
373
|
|
|
name: event.message!.name, |
374
|
|
|
requestBody: message, |
375
|
|
|
updateMask: 'cardsV2', |
376
|
|
|
}; |
377
|
|
|
const apiResponse = await callMessageApi('update', request); |
378
|
|
|
if (apiResponse) { |
379
|
|
|
return buildActionResponse('Option is added', 'OK'); |
380
|
|
|
} else { |
381
|
|
|
return buildActionResponse('Failed to add option.', 'UNKNOWN'); |
382
|
|
|
} |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
function getEventPollState(event: chatV1.Schema$DeprecatedEvent) { |
386
|
|
|
const parameters = event.common?.parameters; |
387
|
|
|
const state = parameters?.['state']; |
388
|
|
|
if (!state) { |
389
|
|
|
throw new ReferenceError('no valid state in the event'); |
390
|
|
|
} |
391
|
|
|
return JSON.parse(state); |
392
|
|
|
} |
393
|
|
|
|